<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="../resources/utils.js"></script>
<script src="resources/utils.sub.js"></script>
<script src="/common/subset-tests-by-key.js"></script>

<meta name="variant" content="?include=defaultPredicate">
<meta name="variant" content="?include=hrefMatches">
<meta name="variant" content="?include=and">
<meta name="variant" content="?include=or">
<meta name="variant" content="?include=not">
<meta name="variant" content="?include=invalidPredicate">
<meta name="variant" content="?include=linkInShadowTree">
<meta name="variant" content="?include=linkHrefChanged">
<meta name="variant" content="?include=newRuleSetAdded">
<meta name="variant" content="?include=selectorMatches">
<meta name="variant" content="?include=selectorMatchesScopingRoot">
<meta name="variant" content="?include=selectorMatchesInShadowTree">
<meta name="variant" content="?include=selectorMatchesDisplayNone">
<meta name="variant" content="?include=selectorMatchesDisplayLocked">
<meta name="variant" content="?include=unslottedLink">
<meta name="variant" content="?include=immediateMutation">
<meta name="variant" content="?include=baseURLChangedBySameDocumentNavigation">
<meta name="variant" content="?include=baseURLChangedByBaseElement">
<meta name="variant" content="?include=linkToSelfFragment">

<body>
<script>
  setup(() => assertSpeculationRulesIsSupported());

  subsetTestByKey('defaultPredicate', promise_test, async t => {
    const url = getPrefetchUrl();
    addLink(url);
    insertDocumentRule();
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test document rule with no predicate');

  subsetTestByKey('hrefMatches', promise_test, async t => {
    insertDocumentRule({ href_matches: '*\\?uuid=*&foo=bar' });

    const url_1 = getPrefetchUrl({foo: 'bar'});
    addLink(url_1);
    const url_2 = getPrefetchUrl({foo: 'buzz'});
    addLink(url_2)
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url_1), 1);
    assert_equals(await isUrlPrefetched(url_2), 0);
  }, 'test href_matches document rule');

  subsetTestByKey('and', promise_test, async t => {
    insertDocumentRule({
      'and': [
        { href_matches: '*\\?*foo=bar*' },
        { href_matches: '*\\?*fizz=buzz*' }]
    });

    const url_1 = getPrefetchUrl({foo: 'bar'});
    const url_2 = getPrefetchUrl({fizz: 'buzz'});
    const url_3 = getPrefetchUrl({foo: 'bar', fizz: 'buzz'});
    [url_1, url_2, url_3].forEach(url => addLink(url));
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url_1), 0);
    assert_equals(await isUrlPrefetched(url_2), 0);
    assert_equals(await isUrlPrefetched(url_3), 1);
  }, 'test document rule with conjunction predicate');

  subsetTestByKey('or', promise_test, async t => {
    insertDocumentRule({
      'or': [
        { href_matches: '*\\?*foo=bar*' },
        { href_matches: '*\\?*fizz=buzz*' }]
    });

    const url_1 = getPrefetchUrl({ foo: 'buzz' });
    const url_2 = getPrefetchUrl({ fizz: 'buzz' });
    const url_3 = getPrefetchUrl({ foo: 'bar'});
    [url_1, url_2, url_3].forEach(url => addLink(url));
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url_1), 0);
    assert_equals(await isUrlPrefetched(url_2), 1);
    assert_equals(await isUrlPrefetched(url_3), 1);
  }, 'test document rule with disjunction predicate');

  subsetTestByKey('not', promise_test, async t => {
    insertDocumentRule({ not: { href_matches: '*\\?uuid=*&foo=bar' } });

    const url_1 = getPrefetchUrl({foo: 'bar'});
    addLink(url_1);
    const url_2 = getPrefetchUrl({foo: 'buzz'});
    addLink(url_2)
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url_1), 0);
    assert_equals(await isUrlPrefetched(url_2), 1);
  }, 'test document rule with negation predicate');

  subsetTestByKey('invalidPredicate', promise_test, async t => {
    const url = getPrefetchUrl();
    addLink(url);
    insertDocumentRule({invalid: 'predicate'});
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url), 0);
  }, 'invalid predicate should not throw error or start prefetch');

  subsetTestByKey('linkInShadowTree', promise_test, async t => {
    insertDocumentRule();

    // Create shadow root.
    const shadowHost = document.createElement('div');
    document.body.appendChild(shadowHost);
    const shadowRoot = shadowHost.attachShadow({mode: 'open'});

    const url = getPrefetchUrl();
    addLink(url, shadowRoot);
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test that matching link in a shadow tree is prefetched');

  subsetTestByKey('linkHrefChanged', promise_test, async t => {
    insertDocumentRule({href_matches: "*\\?*foo=bar*"});

    const url = getPrefetchUrl();
    const link = addLink(url);
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 0);

    const matching_url = getPrefetchUrl({foo: 'bar'});
    link.href = matching_url;
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(matching_url), 1);
  }, 'test that changing the href of an invalid link to a matching value triggers a prefetch');

  subsetTestByKey('newRuleSetAdded', promise_test, async t => {
    insertDocumentRule({href_matches: "*\\?*foo=bar*"});
    const url = getPrefetchUrl({fizz: "buzz"});
    addLink(url);
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 0);

    insertDocumentRule({href_matches: "*\\?*fizz=buzz*"});
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test that adding a second rule set triggers prefetch');

  subsetTestByKey('selectorMatches', promise_test, async t => {
    insertDocumentRule({ selector_matches: 'a.important-link' });

    const url_1 = getPrefetchUrl({foo: 'bar'});
    const importantLink = addLink(url_1);
    importantLink.className = 'important-link';
    const url_2 = getPrefetchUrl({foo: 'buzz'});
    addLink(url_2)
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url_1), 1);
    assert_equals(await isUrlPrefetched(url_2), 0);
  }, 'test selector_matches document rule');

  subsetTestByKey('selectorMatchesScopingRoot', promise_test, async t => {
    insertDocumentRule({ selector_matches: ':root > body > a' });

    const url_1 = getPrefetchUrl({ foo: 'bar' });
    addLink(url_1);

    const url_2 = getPrefetchUrl({ foo: 'buzz' });
    const extraContainer = document.createElement('div');
    document.body.appendChild(extraContainer);
    addLink(url_2, extraContainer);

    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url_1), 1);
    assert_equals(await isUrlPrefetched(url_2), 0);
  }, 'test selector_matches with :root');

  // 'selector_matches' should use the shadowRoot as the scoping root when
  // matching links inside a shadow tree.
  subsetTestByKey('selectorMatchesInShadowTree', promise_test, async t => {
    insertDocumentRule({ selector_matches: ':scope a.important-link' });

    // Create shadow root.
    const shadowHost = document.createElement('div');
    document.body.appendChild(shadowHost);
    const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

    const url = getPrefetchUrl();
    const link = addLink(url, shadowRoot);
    link.className = 'important-link';
    await new Promise(resolve => t.step_timeout(resolve, 2000));

    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test selector_matches with link inside shadow tree');

  subsetTestByKey('selectorMatchesDisplayNone', promise_test, async t => {
    const style = document.createElement('style');
    style.innerText = ".important-section { display: none; }";
    document.head.appendChild(style);
    insertDocumentRule();

    const importantSection = document.createElement('div');
    importantSection.className = 'important-section';
    document.body.appendChild(importantSection);
    const url = getPrefetchUrl();
    addLink(url, importantSection);

    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 0);

    style.remove();
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test selector_matches with link inside display:none container');

  subsetTestByKey('selectorMatchesDisplayLocked', promise_test, async t => {
    const style = document.createElement('style');
    style.innerText = ".important-section { content-visibility: hidden; }";
    document.head.appendChild(style);
    insertDocumentRule({ selector_matches: '.important-section a' });

    const importantSection = document.createElement('div');
    importantSection.className = 'important-section';
    document.body.appendChild(importantSection);
    const url = getPrefetchUrl();
    addLink(url, importantSection);

    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 0);

    style.remove();
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test selector_matches with link inside display locked container');

  subsetTestByKey('unslottedLink', promise_test, async t => {
    insertDocumentRule();

    // Create shadow root.
    const shadowHost = document.createElement('div');
    document.body.appendChild(shadowHost);
    const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

    // Add unslotted link.
    const url = getPrefetchUrl();
    addLink(url, shadowHost);

    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 0);
  }, 'test that unslotted link never matches document rule');

  subsetTestByKey('immediateMutation', promise_test, async t => {
    // Add a link and allow it to get its style computed.
    // (Double RAF lets this happen normally.)
    const url = getPrefetchUrl();
    const link = addLink(url, document.body);
    await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));

    // Add a document rule and then immediately change the DOM to make it match.
    insertDocumentRule({ selector_matches: '.late-class *' });
    document.body.className = 'late-class';

    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 1);
  }, 'test that selector_matches predicates respect changes immediately');

  const baseURLChangedTestFixture = (testName, modifyBaseURLFunc) => {
    return subsetTestByKey(testName, promise_test, async t => {
      const url = getPrefetchUrl();
      const link = addLink(url);
      const url_pattern_string = `prefetch.py${url.search}`;

      // Insert a document rule with a url pattern predicate that uses a
      // relative URL which will not match with |url|, due to |document.baseURI|
      // being different from |url|'s path.
      assert_false((new URLPattern(url_pattern_string, document.baseURI)).test(url));
      insertDocumentRule({ href_matches: url_pattern_string });
      await new Promise(resolve => t.step_timeout(resolve, 2000));
      assert_equals(await isUrlPrefetched(url), 0);

      // Change the baseURL of the document to |url|. |url| should now be
      // prefetched.
      modifyBaseURLFunc(url);
      assert_true((new URLPattern(url_pattern_string, document.baseURI)).test(url));
      await new Promise(resolve => t.step_timeout(resolve, 2000));
      assert_equals(await isUrlPrefetched(url), 1);
    });
  }

  baseURLChangedTestFixture('baseURLChangedBySameDocumentNavigation', url => {
    history.pushState({}, "", url);
  });

  baseURLChangedTestFixture('baseURLChangedByBaseElement', url => {
    const base = document.createElement('base');
    base.href = url;
    document.head.appendChild(base);
  });

  subsetTestByKey('linkToSelfFragment', promise_test, async t => {
    const url = getPrefetchUrl();
    history.pushState({}, "", url);
    addLink(new URL('#fragment', url));
    insertDocumentRule();
    await new Promise(resolve => t.step_timeout(resolve, 2000));
    assert_equals(await isUrlPrefetched(url), 0);
  }, 'test that a fragment link to the current document does not prefetch');

</script>
</body>
